Skip to content

utf8ansi terminal renderer#25

Open
subtleGradient wants to merge 63 commits intoansilove:masterfrom
effect-native:utf8ansi-terminal
Open

utf8ansi terminal renderer#25
subtleGradient wants to merge 63 commits intoansilove:masterfrom
effect-native:utf8ansi-terminal

Conversation

@subtleGradient
Copy link

utf8+ansi renderer
instead of rendering to PNG, it renders to UTF-8 chars + ANSI that doesn't break when someone changes their terminal theme

good for converting BBS art into Linux / macOS terminal art

subtleGradient and others added 30 commits September 18, 2025 19:02
This commit adds the AGENTS.md file, which contains guidelines for contributing to the project. This includes information on project
structure, build and test commands, coding style, testing guidelines, and commit/pull request guidelines.
- Extended ansilove.h with ANSILOVE_MODE_TERMINAL enum
- Added ansilove_terminal() and ansilove_terminal_emit() function signatures
- Created cp437_unicode.h: Complete CP437→Unicode mapping table with UTF-8 encoder
  - Fixed box-drawing character mappings (0xB0-0xB7, 0xB8-0xBF, 0xC0-0xCF)
  - Verified 256-entry table with no duplicates
- Created dos_colors.h: CGA/EGA 16-color palette with ANSI256 conversion
  - dos_color_to_ansi256(): Maps DOS colors to ANSI 256-color codes
  - rgb_to_ansi256(): Generic RGB→ANSI 256 conversion using 6×6×6 cube
  - dos_palette_init(): Initializes color lookup
- Created src/terminal.c: Core terminal backend infrastructure
  - terminal_grid structure: Cell-based grid accumulation
  - ANSI parser: Reuses cursor positioning and SGR logic from existing parser
  - terminal_emit_cell(): Converts cells to UTF-8+ANSI SGR codes
  - ansilove_terminal(): Parses ANSI file, accumulates in grid
  - ansilove_terminal_emit(): Returns UTF-8+ANSI output buffer
- Updated CMakeLists.txt: Added terminal.c to build
- Compiles cleanly with no warnings (gcc -std=c99 -Wall -Wextra)
- test_utf8_emit.c: Validates CP437→UTF-8 conversion and DOS→ANSI256 color mapping
  - Tests shading characters (0xB0-0xB2): ░, ▒, ▓
  - Tests box-drawing characters (0xC0, 0xC3): └, ├
  - Verifies UTF-8 encoding correctness (1-3 bytes per character)
  - Validates all 16 DOS colors map to correct ANSI 256 codes
- test_terminal.c: Integration test harness (skeleton for full linking)
  - Demonstrates how to call ansilove_terminal() API
  - Shows ansilove_terminal_emit() usage pattern
  - Ready for full build once dependencies resolved
- Overview of new terminal rendering capability
- Phase 1 foundation architecture (API, CP437 table, color palette)
- Phase 2 backend implementation (grid accumulation, ANSI parsing)
- Complete API usage examples
- Output format specification
- Testing instructions and validation
- Known limitations and future enhancements
- File structure and references
- Fix STATE_END collision bug (was 2, now 3)
- Fix SGR color parsing (remove terminating char from params)
- Fix CP437 Unicode table (correct block characters)
- Add viewer.c minimal terminal renderer
- Add clean_minimal.c (no GD dependencies)
- Add SAUCE metadata support in src/sauce.h
- Add example_terminal.c demonstration
- Document specs in .specs/utf8ansi/

Tested on fire-43 ANSI art collection (162KB files).
Verified with ansee PNG generation:
- #43_FIRE.ANS: 1416x1532px, 188KB
- Text output matches cat-ans reference
- 7 distinct colors in US-JELLY.ANS output
- Tested on 20+ fire-43 ANSI files successfully
Terminal output verified against official PNG renderer.
Content matches, dimensions differ due to rendering method.

AVG-LARA.ANS memory error is follow-up item.
- Replace hardcoded ANSI256 color mappings with RGB-based conversion
- Use rgb_to_ansi256() to map DOS palette colors to ANSI 6x6x6 cube
- Ensures accurate color representation in terminal output
- Add ansilove-utf8ansi-ansee pipeline tool for testing
- Test with fire-43 ANSI collection
- ansee uses anti-aliased TrueType rendering, not pixel-perfect bitmaps
- Creates 700+ gradient colors vs 9 pure DOS colors
- ANSI256 color mapping provides sufficient terminal fidelity
- Pixel-perfect comparison requires GD-based PNG backend
- UTF-8+ANSI terminal mode is correct for its use case
- ansilove-utf8ansi: Convert DOS ANSI to UTF-8+ANSI for Linux terminals
- Supports direct printing to terminal (stdout)
- Supports saving to .utf8ansi files for later viewing
- demo-utf8ansi.sh: Demonstration script for peer reviewers
- Shows format conversion, terminal display, and ansee integration
- Works with fire-43 ANSI art collection

Usage:
  ansilove-utf8ansi file.ans                 # Print to terminal
  ansilove-utf8ansi file.ans > file.utf8ansi # Save to file
  cat file.utf8ansi                          # View saved file
  ansee file.utf8ansi -o file.png            # Render to PNG
- Setup instructions for reviewers
- Testing procedures with fire-43 collection
- Expected outputs and success criteria
- Known issues documentation (AVG-LARA bug, ansee limitations)
- Example session walkthrough
Increased output buffer size calculation from 8x to 50x per cell.

Each cell can have up to ~46 bytes of ANSI codes:
- Reset (4) + Bold (4) + Blink (4) + Invert (4)
- FG color (13) + BG color (13) + UTF-8 char (4)

Old formula: (rows) * (cols + 2) * 8 = ~140KB for 214 rows
New formula: (rows) * (cols + 2) * 50 = ~880KB for 214 rows

Fixes: AVG-LARA.ANS (214 lines, 47KB)
Result: All 26/26 fire-43 files now convert successfully

Tested:
- AVG-LARA.ANS: 216 lines of UTF-8+ANSI output (186KB)
- All fire-43 collection: 26/26 ✓
- Removed AVG-LARA bug from known issues (fixed)
- Updated test expectations to 26/26 success
- All files in fire-43 collection now convert successfully
DOS ANSI uses SGR 1 (bold) to select bright color palette:
- Without bold: colors 0-7 (base palette)
- With bold: colors 8-15 (bright palette)

Example: ESC[1;30m = Bold + Black -> Dark Gray (DOS 8)

Changed: When bold is active and foreground < 8, add 8 to get bright variant
Result: Correct color rendering for all DOS ANSI art

Verified with AVG-LARA.ANS which heavily uses bold for bright colors
All 26/26 fire-43 files still work correctly
Replaced algorithmic RGB conversion with pre-calculated lookup table
of closest ANSI 256-color matches for each DOS color.

Improvements:
- DOS 6 (Brown): ANSI 130 -> 136 (better match)
- DOS 7 (Light Gray): ANSI 188 -> 248 (3.5 vs 18.7 distance)
- DOS 8 (Dark Gray): ANSI 59 -> 240 (5.2 vs 34.9 distance)
- DOS 9 (Light Blue): ANSI 63 -> 105 (better match)
- DOS 10 (Light Green): ANSI 83 -> 120 (better match)
- DOS 11 (Light Cyan): ANSI 87 -> 123 (better match)
- DOS 12 (Light Red): ANSI 203 -> 210 (better match)
- DOS 13 (Light Magenta): ANSI 207 -> 213 (better match)
- DOS 14 (Yellow): ANSI 227 -> 228 (better match)

Note: Perfect DOS color representation impossible with ANSI 256-palette
due to inherent RGB value differences (DOS: 0,85,170,255 vs ANSI: 0,51,102,153,204,255).
These are the mathematically closest matches possible.

All 26/26 fire-43 files tested ✓
Replaced ANSI 256-color approximations with direct RGB truecolor codes.
This gives pixel-perfect DOS color representation in modern terminals.

Format: ESC[38;2;R;G;Bm (foreground) and ESC[48;2;R;G;Bm (background)

Examples:
- DOS 6 (Brown): 38;2;170;85;0m (#AA5500) - skin tones now correct
- DOS 8 (Dark Gray): 38;2;85;85;85m (#555555)
- DOS 14 (Yellow): 38;2;255;255;85m (#FFFF55)

Benefits:
- Exact DOS color values (no approximation)
- Works on all modern terminals with truecolor support
- Fixes skin color rendering (brown was showing as blue)

All 26/26 fire-43 files tested ✓
DOS ANSI SGR codes don't map sequentially to DOS palette:
- SGR 30-37 map to DOS colors: 0,4,2,6,1,5,3,7 (not 0-7)
- SGR 33 (yellow) -> DOS 6 (brown) - skin tones now correct!
- SGR 34 (blue) -> DOS 1 (blue)
- SGR 36 (cyan) -> DOS 3 (cyan)

This matches PC ANSI.SYS standard color ordering.

Fixes: Skin colors showing as blue (was using DOS 3 instead of DOS 6)
All 26/26 fire-43 files tested ✓
Empty/uninitialized grid cells have character=0 (CP437 NULL).
cp437_to_utf8(0x00) outputs U+0000 which renders as nothing.

Fixed: Convert character=0 to 0x20 (space) before UTF-8 conversion.

Result: Proper spacing in output, matches cat-ans character count.

Before: 2083 spaces (missing 2463 spaces)
After: 4657 spaces (matches expected output)

All 26/26 fire-43 files tested ✓
Don't pad lines to full grid width - stops at last non-space character.
Prevents terminal resize from breaking ANSI art layout.

Before: Outputting full 80+ columns with trailing spaces
After: Trim each line to actual content width

Result: Terminal resize no longer breaks rendering

All 26/26 fire-43 files tested ✓
Insert ESC[0m before each newline to reset all attributes.
Prevents background colors from extending to terminal edge.

Ensures clean line breaks regardless of terminal width.

All 26/26 fire-43 files tested ✓
Can now process multiple ANSI files in one command:
  ./ansilove-utf8ansi file1.ans file2.ans file3.ans

Outputs all files sequentially to stdout.
Optional columns parameter works as last argument.

Usage:
  ansilove-utf8ansi *.ans
  ansilove-utf8ansi file1.ans file2.ans 80

All 26/26 fire-43 files tested ✓
Detect and stop processing at DOS EOF marker (Ctrl+Z, 0x1A).
This removes SAUCE records and COMNT blocks from output.

SAUCE structure:
- EOF marker (0x1A)
- Optional COMNT blocks
- SAUCE record (128 bytes)

Now stops at 0x1A, preventing metadata from rendering as art.

All 26/26 fire-43 files tested - no SAUCE text in output ✓
Simulates dial-up BBS modem speeds with authentic byte delays.

Features:
- --speed=BAUD: Simulate modem (300, 1200, 2400, 9600, 14400, 28800, etc.)
- --help: Show usage information
- Calculation: baud/10 bytes/sec (accounting for 8N1 framing)

Examples:
  ./ansilove-utf8ansi --speed=2400 file.ans  # Slow 2400 baud
  ./ansilove-utf8ansi --speed=28800 file.ans # Fast 28.8k modem

Delay per byte at common speeds:
- 2400 baud: 4.2ms/byte (authentic slow modem feel)
- 9600 baud: 1.0ms/byte
- 28800 baud: 0.3ms/byte (nice balance)

All 26/26 fire-43 files tested ✓
UTF8+ANSI output averages 4.73x larger than DOS ANSI due to:
- UTF-8 multi-byte encoding (vs CP437 single-byte)
- 24-bit RGB color codes (vs 2-char SGR codes)
- Additional SGR attributes (bold, reset)

Scale factor maintains childhood BBS experience:
- --speed=2400 feels like authentic 2400 baud (0.88ms/byte)
- --speed=9600 feels like authentic 9600 baud (0.22ms/byte)

Tested on fire-43 collection - speed feels right!

Calculation: effective_baud = requested_baud * 4.73
…ground option

- Read SAUCE record to auto-detect column width (fixes wide ANSI files like 110/160 columns)
- Add --transparent-bg option to use ANSI black (ESC[40m) instead of 24-bit black for terminal transparency
- Default to 24-bit truecolor for accurate DOS palette reproduction
- Detect runs of 4+ consecutive spaces/empty cells
- Output ESC[<n>C (cursor forward) instead of individual spaces
- Significantly reduces output size for ANSI art with sparse content
- Fixes rendering issues with files like 13-ROPES.ANS that use cursor positioning
- Removed ESC[<n>C cursor positioning from output
- Output clean UTF-8+ANSI with actual spaces from parsed grid
- Terminal mode should emit character data, not positioning commands
- Maintains correct rendering while providing clean, parseable output
- Default to transparent background (truecolor=0)
- Skip empty/space cells and use ESC[<n>C cursor positioning
- Preserves original ANSI semantics for files with sparse content
- Fixes line wrapping issues in files like AK-CRYPT.ANS, 13-ROPES.ANS, AVG-ELKO.ANS
- Default behavior uses ANSI black (ESC[40m) for transparent backgrounds
- --truecolor flag enables opaque 24-bit RGB black (ESC[48;2;0;0;0m)
- More intuitive naming: presence of flag = opaque colors
Copilot AI review requested due to automatic review settings October 24, 2025 04:09
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds a UTF-8+ANSI terminal rendering mode to libansilove, enabling DOS ANSI art files to be displayed directly in modern terminal emulators without requiring PNG conversion. The implementation includes CP437-to-Unicode character mapping, DOS-to-ANSI256 color conversion, and SAUCE metadata parsing for automatic width detection.

Key changes:

  • New terminal rendering backend with grid-based accumulation and UTF-8 output generation
  • ANSI escape sequence parser supporting cursor positioning and SGR attributes
  • Complete CP437 character set mapping to Unicode with box-drawing support

Reviewed Changes

Copilot reviewed 41 out of 46 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/terminal.c Core terminal backend with ANSI parser, grid accumulation, and UTF-8 emission
src/sauce.h SAUCE metadata parser for extracting file dimensions and metadata
src/dos_colors.h DOS CGA/EGA color palette and ANSI256 conversion tables
src/cp437_unicode.h CP437 to Unicode mapping table with UTF-8 encoding function
include/ansilove.h Public API extensions for terminal mode functions
viewer.c CLI tool for converting ANSI files to terminal output with modem speed simulation
example/example_terminal.c Example code demonstrating terminal mode API usage
Test files (multiple) Test programs and ANSI test data files for validation
Documentation files Comprehensive documentation of implementation, testing, and usage
Comments suppressed due to low confidence (1)

sauce.h:1

  • File size parsing uses big-endian byte order, but the comment on line 12 states it should be little-endian. The correct little-endian parsing should be: sauce->filesize = record[103] | (record[104] << 8) | (record[105] << 16) | (record[106] << 24); with indices 103-106 instead of 100-103.

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +92 to +93
sauce->filesize = (record[103] << 24) | (record[102] << 16) |
(record[101] << 8) | record[100];
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File size parsing uses big-endian byte order, but the comment on line 12 states it should be little-endian. The correct little-endian parsing should be: sauce->filesize = record[103] | (record[104] << 8) | (record[105] << 16) | (record[106] << 24); with indices 103-106 instead of 100-103.

Suggested change
sauce->filesize = (record[103] << 24) | (record[102] << 16) |
(record[101] << 8) | record[100];
sauce->filesize = record[103] | (record[104] << 8) | (record[105] << 16) | (record[106] << 24);

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
int main() {
char line[1000];
FILE *f = popen("./ansilove-utf8ansi /home/tom/Downloads/fire-39/H4-2017.ANS 2>&1 | head -1", "r");
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path /home/tom/Downloads/fire-39/H4-2017.ANS makes this test non-portable. Consider using a relative path to a test file in the repository or accepting the path as a command-line argument.

Suggested change
int main() {
char line[1000];
FILE *f = popen("./ansilove-utf8ansi /home/tom/Downloads/fire-39/H4-2017.ANS 2>&1 | head -1", "r");
int main(int argc, char *argv[]) {
char line[1000];
if (argc < 2) {
fprintf(stderr, "Usage: %s <path-to-ANS-file>\n", argv[0]);
return 1;
}
char cmd[1100];
snprintf(cmd, sizeof(cmd), "./ansilove-utf8ansi '%s' 2>&1 | head -1", argv[1]);
FILE *f = popen(cmd, "r");

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +17
int main() {
struct ansilove_ctx ctx;
struct ansilove_options opts;

memset(&ctx, 0, sizeof(ctx));
memset(&opts, 0, sizeof(opts));

ansilove_init(&ctx, &opts);
ansilove_loadfile(&ctx, "/home/tom/Downloads/fire-43/AVG-LARA.ANS");
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path /home/tom/Downloads/fire-43/AVG-LARA.ANS makes this test non-portable. Consider using a relative path to a test file in the repository or accepting the path as a command-line argument.

Suggested change
int main() {
struct ansilove_ctx ctx;
struct ansilove_options opts;
memset(&ctx, 0, sizeof(ctx));
memset(&opts, 0, sizeof(opts));
ansilove_init(&ctx, &opts);
ansilove_loadfile(&ctx, "/home/tom/Downloads/fire-43/AVG-LARA.ANS");
int main(int argc, char *argv[]) {
struct ansilove_ctx ctx;
struct ansilove_options opts;
if (argc < 2) {
fprintf(stderr, "Usage: %s <path-to-ANS-file>\n", argv[0]);
return 1;
}
memset(&ctx, 0, sizeof(ctx));
memset(&opts, 0, sizeof(opts));
ansilove_init(&ctx, &opts);
ansilove_loadfile(&ctx, argv[1]);

Copilot uses AI. Check for mistakes.
return 1;
}

if (ansilove_loadfile(&ctx, "/home/tom/Downloads/fire-43/AVG-LARA.ANS") != 0) {
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path /home/tom/Downloads/fire-43/AVG-LARA.ANS makes this test non-portable. Consider using a relative path to a test file in the repository or accepting the path as a command-line argument.

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +12
int main() {
struct ansilove_ctx ctx;
struct ansilove_options opts = { 0 };

if (ansilove_init(&ctx, &opts) != 0)
return 1;

if (ansilove_loadfile(&ctx, "/home/tom/Downloads/fire-39/CAL24-01.ANS") != 0)
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path /home/tom/Downloads/fire-39/CAL24-01.ANS makes this test non-portable. Consider using a relative path to a test file in the repository or accepting the path as a command-line argument.

Suggested change
int main() {
struct ansilove_ctx ctx;
struct ansilove_options opts = { 0 };
if (ansilove_init(&ctx, &opts) != 0)
return 1;
if (ansilove_loadfile(&ctx, "/home/tom/Downloads/fire-39/CAL24-01.ANS") != 0)
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <ANS file path>\n", argv[0]);
return 1;
}
struct ansilove_ctx ctx;
struct ansilove_options opts = { 0 };
if (ansilove_init(&ctx, &opts) != 0)
return 1;
if (ansilove_loadfile(&ctx, argv[1]) != 0)

Copilot uses AI. Check for mistakes.
Comment on lines +5 to +12
int main() {
struct ansilove_ctx ctx;
struct ansilove_options opts = { 0 };

if (ansilove_init(&ctx, &opts) != 0)
return 1;

if (ansilove_loadfile(&ctx, "/home/tom/Downloads/fire-39/H4-2017.ANS") != 0)
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path /home/tom/Downloads/fire-39/H4-2017.ANS makes this test non-portable. Consider using a relative path to a test file in the repository or accepting the path as a command-line argument.

Suggested change
int main() {
struct ansilove_ctx ctx;
struct ansilove_options opts = { 0 };
if (ansilove_init(&ctx, &opts) != 0)
return 1;
if (ansilove_loadfile(&ctx, "/home/tom/Downloads/fire-39/H4-2017.ANS") != 0)
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s <input_file>\n", argv[0]);
return 1;
}
struct ansilove_ctx ctx;
struct ansilove_options opts = { 0 };
if (ansilove_init(&ctx, &opts) != 0)
return 1;
if (ansilove_loadfile(&ctx, argv[1]) != 0)

Copilot uses AI. Check for mistakes.
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);

execlp("/home/tom/.cargo/bin/ansee", "ansee", "-o", output_png, NULL);
Copy link

Copilot AI Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded absolute path /home/tom/.cargo/bin/ansee makes this program non-portable. Use execlp("ansee", "ansee", "-o", output_png, NULL) to search for ansee in the user's PATH instead.

Suggested change
execlp("/home/tom/.cargo/bin/ansee", "ansee", "-o", output_png, NULL);
execlp("ansee", "ansee", "-o", output_png, NULL);

Copilot uses AI. Check for mistakes.
@subtleGradient
Copy link
Author

PLENTY of cleanup work left to go before this is ready to land (if ever); should probably be a separate project entirely that depends on libansilove instead; but here's what we've got if y'all want to check it out :D

subtleGradient and others added 18 commits October 26, 2025 10:07
Document the completed fix for black gaps appearing in colored background regions.
Includes root cause analysis, solution details, test cases, and verification checklist.
Critical fix on line 537 of terminal.c to prevent dangling pointer bug.
- Add BUILD_INSTRUCTIONS.md with comprehensive build and usage guide
- Remove empty ansilove-utf8ansi and ansilove-utf8ansi-ansee files from root
- Actual binaries are in build/ directory (generated, not tracked)
- Add UTF-8 ANSI Terminal Mode section with key files and APIs
- Include terminal mode build commands and test procedures
- Document critical background color gap regression test
- Add utf8ansi-terminal branch-specific context with current status
- Add NEXT_SESSION_START_HERE.md for quick session handoff
- Reference all documentation files for future development
- UTF8ANSI_VALIDATION.md: Scientific end-to-end comparison protocol
  - Single-file determinism testing (NEWS-50.ANS)
  - Documented tool versions, metrics, findings
  - Confirms pipeline determinism, identifies line-count inflation bug

- tools/batch_validate.sh: Automated corpus testing
  - Processes all 131 ANSI files
  - Compares ansilove CLI vs utf8ansi+ansee
  - Generates CSV with dimensions, deltas, SAUCE metadata

- tools/analyze_batch.py: Result analysis
  - Identifies best/worst matches by line delta
  - Statistical summaries (avg delta: 114 lines)
  - SAUCE metadata correlation

- HUMAN_REVIEW_INSTRUCTIONS.md: Visual inspection protocol for Bramwell
  - Prioritized sample list (perfect matches vs worst cases)
  - Assessment criteria (color, structure, artifacts)
  - Expected findings based on automated analysis

- ansilove-utf8ansi-ansee.c: Use ansee from PATH instead of hardcoded path

Results: 10 perfect line-count matches, avg 114-line inflation across corpus.
**Problem (discovered by Bramwell):**
Text was splitting incorrectly across lines. Example: '50th anniversary'
rendered as '50t' on one line and 'h anniversary' on the next. This
affected 92% of corpus files (121/131).

**Root Cause:**
DOS ANSI art uses CR-LF-ESC[A (0d 0a 1b5b41) to reposition cursor for
multi-pass drawing. Parser processed LF immediately (row++), then later
ESC[A (row--). Characters written between these operations landed on
wrong rows, inflating max_row by 3-4x actual content height.

**Solution:**
Introduced pending_lf flag to defer row increment until next character
confirms it's not a cursor positioning command. If ESC[A/B/H/f follows
LF, cancel or apply the pending increment appropriately.

**Impact (measured across 131-file corpus):**
- BS-ROCK1.ANS: 499→134 lines (was 365 over, now 1 under SAUCE)
- RD-MOOSE.ANS: 499→102 lines (was 396 over, now 1 under SAUCE)
- Average delta: +114→-3.8 lines (97% improvement)
- Height ratio: 3.16x→1.37x (near-perfect)

**Validation:**
- Automated: Batch tested 131 files, most now 0-2 line delta
- Human (Bramwell): Visually confirmed fix on BS-ROCK1.ANS - text no
  longer splits, art renders perfectly
- Regression check: High-confidence files (W7-PHAR1) still perfect

Changes:
- src/terminal.c: Pending LF logic, cancel on cursor positioning
- tools/: Confidence analysis, Bramwell verification protocol
- UTF8ANSI_VALIDATION.md: CR-LF-CursorUp bug documentation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants